﻿
using Boilen.Primitives;
using BoilenEditor.Properties;
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Windows;


namespace BoilenEditor.Primitives {

    internal static class Ex {

        public const int TabSize = 4;
        public static readonly string TabString = new string( ' ', TabSize );
        public static readonly int NewLineLength = Environment.NewLine.Length;

        private static readonly string Quote = '"'.ToString( );
        private static readonly string Escape = '\\'.ToString( );
        private static readonly string EscapedQuote = Escape + Quote;
        private static readonly string EscapedEscape = Escape + Escape;
        private static readonly char[] NewLineArray = Environment.NewLine.ToCharArray( );
        private static readonly TypeRepository TypeRepository = new TypeRepository( );
        private static readonly char[] InvalidPathCharacters = Path.GetInvalidPathChars( );

        private static readonly string BoilenApplicationData = Path.Combine(
            Environment.GetFolderPath( Environment.SpecialFolder.LocalApplicationData ),
            "Boilen"
        );

        private static readonly string ReferencedAssemblyCache = Path.Combine(
            BoilenApplicationData,
            "_referenced_assembly_cache_"
        );


        [DebuggerHidden]
        public static bool HasValue<T>( this T item )
            where T : class {
            return !object.ReferenceEquals( item, null );
        }

        [DebuggerHidden]
        public static R GetValueOrDefault<T, R>( this T item, Func<T, R> getValue, R defaultValue )
            where T : class {
            return item.HasValue( )
                ? getValue( item )
                : defaultValue;
        }


        public static string GetTypeName( this Type type ) {
            return TypeRepository.GetTypeName( type );
        }

        public static string GetValueString( string value, Type valueType ) {
            string s = (value ?? "").Trim( );

            if( valueType.IsEnum ) {
                s = valueType.Name + '.' + s;
            }
            else if( valueType == typeof( bool ) ) {
                s = s.ToLowerInvariant( );
            }
            else if( valueType == typeof( Type ) ) {
                s = "typeof(" + s + ")";
            }
            else if( valueType == typeof( string ) ) {
                s = s.Replace( Escape, EscapedEscape )
                     .Replace( Quote, EscapedQuote );
                s = Quote + s + Quote;
            }
            else if( valueType == typeof( IEnumerable<string> ) && value != "null" ) {
                string[] entries = s
                    .Split( ',' )
                    .Select( e => GetValueString( e, typeof( string ) ) )
                    .ToArray( );

                s = "new[] { " + string.Join( ", ", entries ) + " }";
            }

            return s;
        }


        public static IEnumerable<T> Enumerate<T>( T root, Func<T, bool> condition, Func<T, T> next ) {
            for( T item = root; condition( item ); item = next( item ) )
                yield return item;
        }

        public static int IndexOf<T>( this IEnumerable<T> collection, T item ) {
            return collection
                .Select( ( e, i ) => object.Equals( e, item ) ? (int?)i : null )
                .Where( i => i.HasValue )
                .FirstOrDefault( )
                ?? -1;
        }

        public static void ForEach<T>( this IEnumerable<T> collection, Action<T> action ) {
            if( collection.HasValue( ) )
                foreach( T item in collection )
                    action( item );
        }

        public static void AddRange<T>( this IList<T> collection, IEnumerable<T> items ) {
            items.ForEach( item => collection.Add( item ) );
        }

        public static ReadOnlyCollection<T> ToReadOnlyCollection<T>( this IEnumerable<T> items ) {
            IList<T> collection = items as IList<T> ?? items.ToArray( );
            return new ReadOnlyCollection<T>( collection );
        }


        public static T GetCustomAttribute<T>( this MemberInfo member )
            where T : Attribute {
            return member
                .GetCustomAttributes( typeof( T ), true )
                .OfType<T>( )
                .SingleOrDefault( );
        }


        public static bool Retry( Action operation, string title, Func<Exception, string> messageBuilder ) {
            return Retry( null, _ => operation( ), title, messageBuilder );
        }

        public static bool Retry( string path, Action<string> operation, string title, Func<Exception, string> messageBuilder ) {
            bool unlocked = false;
            bool success = false;
            while( !success ) {
                try {
                    operation( path );
                    success = true;
                }
                catch( Exception ex ) {
                    // Try to unlock file automatically using source control tool.
                    if( !unlocked && path.HasValue( ) && ex is UnauthorizedAccessException ) {
                        unlocked = true;
                        bool unlockResult = CallSourceControlTool( path, SourceControlAction.Edit );
                        if( unlockResult )
                            continue;
                        else
                            break;
                    }

                    string message = messageBuilder( ex );
                    if( string.IsNullOrEmpty( message ) )
                        throw;

                    MessageBoxResult result = MessageBox.Show( message, title, MessageBoxButton.OKCancel, MessageBoxImage.Warning );
                    if( result != MessageBoxResult.OK )
                        break;
                }
            }

            return success;
        }


        public static bool CallSourceControlTool( string path, SourceControlAction action ) {
            string argumentsPath = string.Format( "SourceControl{0}Arguments", action );
            string actionArguments = (string)Settings.Default[argumentsPath];
            string scPath = GetSourceControlPath( );

            bool result = CallSourceControlTool( scPath, action, actionArguments, path );
            return result;
        }

        private static string GetSourceControlPath( ) {
            string scName = Settings.Default.SourceControlSearchName;
            string scPath = Settings.Default.SourceControlPath;

            if( scName.Length == 0 ) {
                scPath = null;
            }
            else if( !scName.OrdinalEqual( Path.GetFileName( scPath ) ) || !File.Exists( scPath ) ) {
                string[] searchPaths = Environment.GetEnvironmentVariable( "Path" ).Split( new[] { ';' }, StringSplitOptions.RemoveEmptyEntries );
                Array.Reverse( searchPaths );
                scPath =
                    searchPaths
                        .SelectMany( searchPath => Ex.GetFiles( searchPath, scName, SearchOption.TopDirectoryOnly ) )
                        .FirstOrDefault( );

                if( scPath == null ) {
                    var environment = Environment.GetEnvironmentVariables( );
                    scPath =
                        environment.Keys.Cast<string>( )
                            .Where( key => key.StartsWith( "VS", StringComparison.OrdinalIgnoreCase ) )
                            .Select( key => Path.GetFullPath( environment[key].ToString( ) + Path.DirectorySeparatorChar + ".." ) )
                            .SelectMany( searchPath => Ex.GetFiles( searchPath, scName, SearchOption.AllDirectories ) )
                            .FirstOrDefault( );
                }

                Settings.Default.SourceControlPath = scPath ?? "";
                Settings.Default.Save( );
            }

            return scPath;
        }

        private static bool CallSourceControlTool( string scPath, SourceControlAction action, string actionArguments, string path ) {
            if( !scPath.HasValue( ) )
                return true;

            string actionValue = string.Format( actionArguments, path );
            string commonArguments = Settings.Default.SourceControlCommonArguments;
            if( commonArguments.IndexOf( "{0}" ) < 0 )
                commonArguments += " {0}";
            string arguments = string.Format( commonArguments, actionValue );
            ProcessStartInfo startInfo = new ProcessStartInfo {
                FileName = scPath,
                Arguments = arguments,
                UseShellExecute = false,
                RedirectStandardOutput = true,
                RedirectStandardError = true
            };

            Process process = Process.Start( startInfo );
            process.WaitForExit( );

            string output =
                  process.StandardOutput.ReadToEnd( )
                + Environment.NewLine
                + process.StandardError.ReadToEnd( );

            bool unlockSuccess =
                   process.ExitCode == 0
                && output.IndexOf( "error:", StringComparison.OrdinalIgnoreCase ) < 0;

            if( !unlockSuccess ) {
                string message = string.Format(
                    "Could not use source control tool to {1} file {2}:{0}{0}" +
                    "{3} {4}{0}{0}" +
                    "{5}{0}{0}" +
                    "Check your source control settings in the Preferences dialog (client arguments may need to be specified for the command to succeed).",
                    Environment.NewLine,
                    action.ToString( ).ToLowerInvariant( ),
                    Path.GetFileName( path ),
                    Path.GetFileName( scPath ),
                    arguments,
                    output.TrimEnd( )
                );

                MessageBoxResult result = MessageBox.Show( message, "File Checkout Failed", MessageBoxButton.OKCancel, MessageBoxImage.Warning );
                unlockSuccess = result == MessageBoxResult.OK;
            }

            return unlockSuccess;
        }


        public static string ReadFile( string path ) {
            return File.Exists( path )
                 ? string.Join( Environment.NewLine, File.ReadAllLines( path ) )
                 : null;
        }

        public static bool ReadAllLines( string path, string fix, out string[] lines ) {
            string[] localLines = null;
            Action readAllLinesCore = delegate { localLines = File.ReadAllLines( path ); };
            Func<Exception, string> readAllLinesMessageBuilder = ( ex ) => {
                if( ex is IOException )
                    return string.Format(
                        "{1}:{0}{2}{0}{0}{3}",
                        Environment.NewLine, ex.GetType( ).FullName, ex.Message, fix
                    );

                return null;
            };

            bool success = Ex.Retry( readAllLinesCore, "File Read Failed", readAllLinesMessageBuilder );
            lines = localLines;
            return success;
        }

        public static bool CopyFile( string referencePath, string referenceDestination, string fix ) {
            bool isAssembly = Path.GetExtension( referencePath ) == ".dll";
            string alternatePath = Path.Combine( ReferencedAssemblyCache, Path.GetFileName( referencePath ) );
            string path =
                isAssembly && !File.Exists( referencePath ) && File.Exists( alternatePath )
                    ? alternatePath
                    : referencePath;

            Action<string> copyFileCore = delegate( string destination ) { File.Copy( path, destination, true ); };
            Func<Exception, string> copyFileMessageBuilder = ( ex ) => {
                if( ex is IOException )
                    return string.Format(
                        "{1}:{0}{2}{0}{0}{3}",
                        Environment.NewLine, ex.GetType( ).FullName, ex.Message, fix
                    );

                return null;
            };

            bool success = Ex.Retry( referenceDestination, copyFileCore, "File Copy Failed", copyFileMessageBuilder );
            if( success ) {
                FileAttributes attributes = File.GetAttributes( referenceDestination );
                if( attributes.IsFlagged( FileAttributes.ReadOnly ) ) {
                    attributes &= ~FileAttributes.ReadOnly;
                    File.SetAttributes( referenceDestination, attributes );
                }

                if( isAssembly && path != alternatePath ) {
                    try {
                        if( !Directory.Exists( ReferencedAssemblyCache ) )
                            Directory.CreateDirectory( ReferencedAssemblyCache );
                        File.Copy( referenceDestination, alternatePath, overwrite: true );
                    }
                    catch( Exception ex ) {
                        Console.WriteLine( "Unable to cache assembly '{0}': {1}", referencePath, ex );
                    }
                }
            }

            return success;
        }

        public static bool DeleteDirectory( string directoryPath, bool ignoreErrors ) {
            Action<string> deleteDirectoryCore = delegate( string path ) {
                if( Directory.Exists( path ) )
                    Directory.Delete( path, true );
            };
            Func<Exception, string> deleteDirectoryMessageBuilder = ( ex ) => {
                if( ex is IOException && !ignoreErrors )
                    return string.Format(
                        "{1}:{0}{2}",
                        Environment.NewLine, ex.GetType( ).FullName, ex.Message
                    );

                return null;
            };

            try {
                bool success = Ex.Retry( directoryPath, deleteDirectoryCore, "Deleting Temporary Files", deleteDirectoryMessageBuilder );
                return success;
            }
            catch( IOException ) {
                if( ignoreErrors )
                    return false;
                else
                    throw;
            }
        }


        public static string GetShadowDirectory( string fileName ) {
            return Path.Combine( BoilenApplicationData, fileName );
        }

        public static IEnumerable<string> GetParentDirectories( string filePath ) {
            string directory = Path.GetDirectoryName( filePath );
            return Ex.Enumerate(
                directory,
                path => !string.IsNullOrEmpty( path ),
                Path.GetDirectoryName
            );
        }

        public static bool TestFile( string rootDirectory, string filePattern ) {
            return GetFiles( rootDirectory, filePattern, SearchOption.TopDirectoryOnly ).Any( );
        }

        public static IEnumerable<string> GetFiles( string rootDirectory, string filePattern ) {
            return GetFiles( rootDirectory, filePattern, SearchOption.AllDirectories );
        }

        public static IEnumerable<string> GetFiles( string rootDirectory, string filePattern, string directoryPattern ) {
            return GetFiles( rootDirectory, filePattern, directoryPattern, SearchOption.AllDirectories );
        }

        public static IEnumerable<string> GetFiles( string rootDirectory, string filePattern, SearchOption searchOption ) {
            return GetFiles( rootDirectory, filePattern, null, searchOption );
        }

        public static IEnumerable<string> GetFiles( string rootDirectory, string filePattern, string directoryPattern, SearchOption searchOption ) {
            var directories = new Queue<string>( );
            directories.Enqueue( rootDirectory );
            while( directories.Count > 0 ) {
                string directory = directories.Dequeue( );
                if( searchOption == SearchOption.AllDirectories )
                    TryFileSystemOperation( ( ) => Directory.GetDirectories( directory, directoryPattern ?? "*" ).ForEach( d => directories.Enqueue( d ) ) );
                directoryPattern = null;

                var files = TryFileSystemOperation(
                    ( ) => Directory.GetFiles( directory, filePattern, SearchOption.TopDirectoryOnly ),
                    Enumerable.Empty<string>( )
                );
                foreach( string file in files )
                    yield return file;
            }
        }

        private static void TryFileSystemOperation( Action operation ) {
            TryFileSystemOperation<object>(
                ( ) => { operation( ); return null; },
                null
            );
        }

        private static R TryFileSystemOperation<R>( Func<R> operation, R defaultValue ) {
            try { return operation( ); }
            catch( IOException ) { return defaultValue; }
            catch( UnauthorizedAccessException ) { return defaultValue; }
        }


        public static bool IsValidPath( string name ) {
            return !string.IsNullOrEmpty( name )
                && !name.Contains( ' ' )
                && name.IndexOfAny( InvalidPathCharacters ) < 0;
        }


        public static bool IsFlagged( this Enum value, Enum flag ) {
            long lValue = Convert.ToInt64( value );
            long lFlag = Convert.ToInt64( flag );
            return (lValue & lFlag) == lFlag;
        }


        public static IEnumerable<string> ReadLines( this StreamReader reader ) {
            string line;
            while( (line = reader.ReadLine( )).HasValue( ) )
                if( line.Length > 0 )
                    yield return line;
        }


        public static bool OrdinalEqual( this string a, string b ) {
            return string.Compare( a, b, StringComparison.OrdinalIgnoreCase ) == 0;
        }

        public static string With( this string format, params object[] args ) {
            return string.Format( format, args );
        }


        public enum SourceControlAction {
            Add,
            Edit
        };

    }

}
